package HillClimbing;
import java.util.HashMap;
import java.util.Random;



/*
 * Implements Hill-Climbing algorithm
 */
public class HillClimbing{

	/**
	 * 
	 */
	public static abstract class HillClimbingFunction
	{
		public abstract double getScore(IPropertiesObj props);
	}
	
	public static interface IPropertiesObj
	{
		public void setParams(double[] props);
		
		public double[] getParams();
		
		public void save(int index, double score);
		
		public void save(double score);
	}
	
	private HillClimbingParameter[] params;
	
	private double[] bestValues;
	
	private double bestScore;
	
	private double[] values;
	
	private double score;
	
	private HillClimbingFunction climbingFunction;
	
	private HashMap<String,Double> cachedResults;
	
	private IPropertiesObj propsObj;
	
	public HillClimbing(HillClimbingFunction climbingFunc, HillClimbingParameter[] params, IPropertiesObj propsObj)
	{
		this.climbingFunction = climbingFunc;
		this.params = params;
		this.values = propsObj.getParams();
		this.bestValues = this.values.clone();
		cachedResults = new HashMap<String, Double>();
		this.propsObj = propsObj;  
	}
	
	public double getParameterValue(int index)
	{
		return this.bestValues[index];
	}
	
	public void startHillClimbingRandomStarts(int randomStartsToPerform)
	{
		// set best score to - infinity
		bestScore = Double.MIN_VALUE;
		for(int i=0; i<randomStartsToPerform; i++)
		{
			// perform hill climbing try
			startHillClimbing(true);

			// if better score achieved --> keep this values
			if( bestScore < this.score)
			{
				this.bestScore = this.score;
				for(int j=0; j<this.params.length; j++)
				{
					this.bestValues[j] = this.values[j];
				}
			}
		}
		this.propsObj.setParams(this.bestValues);
		this.propsObj.save(bestScore);
	}
	
	public void startHillClimbing(boolean random) {
		// set random values
		if( random)
			setRandomValues();
		
		// get the score for the initial position
		this.score = getScoreForValues();
		while(true)
		{
			double bestScoreDirection = this.score;
			int bestDimension = -1;
			double bestStep = 0.0;
			// climb
			for( int i=0; i<this.params.length; i++)
			{
				HillClimbingParameter param = this.params[i];
				if( param.isInBounds(this.values[i] + param.step))
				{
					this.values[i] += param.step;
					double tmp_score = getScoreForValues();
					System.out.println(this.propsObj + " " + (-tmp_score));
					if( tmp_score > bestScoreDirection)
					{
						bestScoreDirection = tmp_score;
						bestDimension = i;
						bestStep = param.step;
					}
					this.values[i] -= param.step;
				}
				if( param.isInBounds(this.values[i] - param.step))
				{
					this.values[i] -= param.step;
					double tmp_score = getScoreForValues();
					System.out.println(this.propsObj + " " + (-tmp_score));
					if( tmp_score > bestScoreDirection)
					{
						bestScoreDirection = tmp_score;
						bestDimension = i;
						bestStep = -param.step;
					}
					this.values[i] += param.step;
				}
			}
			
			// if no better direction found, break loop
			if( bestDimension < 0)
			{
				System.out.println("reached hill top");
				this.propsObj.setParams(this.values);
				System.out.println(this.propsObj + " " + (-bestScoreDirection));
				this.propsObj.setParams(this.values);
				this.propsObj.save(-bestScoreDirection);
				break;
			}
			else
			{
				this.values[bestDimension] += bestStep; 
				this.propsObj.setParams(this.values);
				System.out.println("moved to " + this.propsObj + " " + (-bestScoreDirection));
				this.score = bestScoreDirection;
			}
		}
	}

	private double getScoreForValues() {
		String key = generateKeyForVector(this.values);
		if( cachedResults.containsKey(key))
			return cachedResults.get(key);
		else
		{
			this.propsObj.setParams(this.values);
			double score = this.climbingFunction.getScore(this.propsObj);
			cachedResults.put(key, score);
			return score;
		}
	}

	private String generateKeyForVector(double[] values2) {
		StringBuffer buf = new StringBuffer();
		for( int i=0; i< values2.length; i++)
		{
			buf.append(values2[i]);
			buf.append(",");
		}
		return buf.toString();
	}

	private void setRandomValues()
	{
		Random rnd = new Random();
		for(int i=0; i<params.length; i++)
		{
			double range = params[i].getRange();
			values[i] =rnd.nextFloat() *( range) + params[i].getLowerBound();
		}
	}
}
